/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License.  You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import io.ByteReader;
import io.ByteWriter;
import io.PacketInputStream;
import io.PacketOutputStream;
import packet.RawPacket;
import packet.in.NormalIMPacket;
import packet.in.SendIMPacket;
import util.Crypter;
import util.HexDumper;
import util.Util;

import java.io.IOException;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Project  : rtx
 * Author   : solosky < solosky772@qq.com >
 * Created  : 4/7/14
 * License  : Apache License 2.0
 */
public class RtxClient {
    private PacketInputStream in;
    private PacketOutputStream out;
    private Socket sk;
    private short nextSeq = 0x0e55;
    private int uin;
    private String uname;
    private String pass;
    private byte[] touchKey, sessionKey;
    private Crypter crypter = new Crypter();
    private Timer timer = new Timer();
    private int authType;


    public void login(String host, int port, String userName, String password) throws IOException {
        sk = new Socket(host, port);
        in = new PacketInputStream(sk.getInputStream());
        out = new PacketOutputStream(sk.getOutputStream());
        pass = password;

        ByteWriter writer = new ByteWriter();
        writer.writeByte(0x02);
        writer.writeUnicode(userName);
        touchKey = Util.randomKey();

        writePacket(0x400, ++nextSeq, writer.toByteArray(), touchKey, true);
    }


    private void writePacket(int cmd, int seq, byte[] data, byte[] key, boolean sendKey) throws IOException {
        System.out.println("---------------------------------------------------------");
        System.out.println("SENT: cmd=0x" + Integer.toHexString(cmd).toUpperCase());
        System.out.print("---------------------------------------------------------");
        System.out.println(HexDumper.dumpHexString(data));

        byte[] raw = crypter.encrypt(data, key);
        if (sendKey) {
            ByteWriter writer = new ByteWriter();
            writer.write(key);
            writer.writeByte(raw.length);
            writer.write(raw);
            raw = writer.toByteArray();
        }

        RawPacket p = new RawPacket();
        p.setCmd(cmd);
        p.setSeq(seq);
        p.setData(raw);
        p.setUin(uin);
        p.setLen(raw.length + 0xe);
        out.writePacket(p);
    }


    private void processTouchReply(RawPacket packet) throws IOException {
        byte[] data = crypter.decrypt(packet.getData(), touchKey);
        System.out.println("touch reply data: " + Util.convertByteToHexString(data));
        ByteReader reader = new ByteReader(data);
        int status = reader.readByte();
        if (status == 0xFE) {
            System.out.println("user not exists!!!");
        }
        if (status == 0x02 || status == 0x0) {
            uin = reader.readInt();
            uname = reader.readUnicode();
            byte[] salt = new byte[4];
            reader.read(salt);
            authType = reader.readByte();
            System.out.println("Touch OK : uin=" + uin + ", uname=" + uname + ",salt=" + Util.convertByteToHexString(salt));
            ByteWriter writer = new ByteWriter();
            if (authType == 0) {
                writer.writeUnicode(uname);
                writer.writeByte(0x01); //authType
                writer.writeToken(crypter.encrypt(salt, passKey2(pass)));
                writer.writeToken(crypter.encrypt(salt, passKey1(pass)));
                writer.write(Util.randomKey()); //???
                writer.writeByte(0);
            } else if (authType == 1) {

                writer.writeUnicode(uname);
                writer.writeByte(0x02); //authType
                ByteWriter tmpWriter = new ByteWriter();
                tmpWriter.writeByte(pass.length());
                tmpWriter.write(pass.getBytes());
                tmpWriter.write(salt);
                writer.writeToken(crypter.encrypt(tmpWriter.toByteArray(), nameKey3(uname)));
                writer.writeByte(0);
                writer.write(Util.randomKey()); //???
                writer.writeByte(0);
            }

            writePacket(0x0412, ++nextSeq, writer.toByteArray(), Util.randomKey(), true);
        } else {
            System.out.println("Unknown touch result code: " + status);
        }
    }


    private void processVerifyReply(RawPacket packet) {
        byte[] data = packet.getData();
        System.out.println(HexDumper.dumpHexString(data));
        int status = data[0];
        if (status == 0) {
            byte[] raw = new byte[packet.getLen() - 0xe - 1];
            System.arraycopy(data, 1, raw, 0, raw.length);
            byte[] plain = null;
            if (authType == 0) {
                plain = crypter.decrypt(raw, passKey2(pass));
            } else if (authType == 1) {
                plain = crypter.decrypt(raw, nameKey3(uname));
            }
            if (plain != null) {
                ByteReader reader = new ByteReader(plain);
                reader.readInt(); //uin
                sessionKey = new byte[0x10];
                reader.read(sessionKey);

                System.out.println("LoginOK: sessionKey=" + Util.convertByteToHexString(sessionKey));

                startKeepAlive();

            } else {
                System.out.println("Decrypt data Failed!!!!");
            }
        } else {
            System.out.println("Login Failed: status=" + status);
        }
    }

    private void processNormalIM(RawPacket packet) throws IOException {
        NormalIMPacket imPacket = new NormalIMPacket(decryptPacket(packet, sessionKey));
        System.out.println(imPacket);

        ByteWriter writer = new ByteWriter();
        writer.write(Util.convertHexStringToByte("00 00 00 00 00 00 01"));
        writer.writeShort(imPacket.getImId());
        writer.writeUnicode(imPacket.getImType());
        writer.writeByte(0);
        writer.writeUnicode(imPacket.getImType2());
        writer.writeUnicode(imPacket.getTo());
        writer.writeUnicode(imPacket.getFrom());
        writer.writeByte(0);
        writer.writeUnicode("OK");
        // writer.write(Util.convertHexStringToByte("00 06 4F 00 4B 00 00 00"));

        writePacket(0xc02, packet.getSeq(), writer.toByteArray(), sessionKey, false);

        sendMsg(imPacket.getFrom(), "收到啦 ~ " + (new Date()));


    }


    private void sendMsg(String to, String msg) throws IOException {
        String body = "<RTXCData><Item Key=\"Mode\" Type=\"Long\">0</Item><Item Key=\"Content\" Type=\"String\">&lt;RTXMsg Ver=\"1.0\"&gt;\n" +
                "&lt;Content&gt;\n" +
                "&lt;Txt&gt;" + msg + "&lt;/Txt&gt;\n" +
                "&lt;Font Name=\"宋体\" CharSet=\"134\" PAF=\"0\" Size=\"9\" YOffset=\"0\" Effects=\"1073741824\" Color=\"0\"/&gt;\n" +
                "&lt;/Content&gt;\n" +
                "&lt;/RTXMsg&gt;\n" +
                "<Item Key=\"Initiator\" Type=\"String\">" + uname + "</Item><Item Key=\"Key\" Type=\"String\">{589E22CE-91E2-4FB5-97C6-2D1DE253C539}</Item><Item Key=\"im_message_id\" Type=\"Buffer\">EAAAANSTcL/D4jxDhS9VIr0khiV=</Item></RTXCData>";


        SendIMPacket auto = new SendIMPacket();
        auto.setImType("Tencent.RTX.IM");
        auto.setImType2("Tencent.RTX.IM");
        auto.setFrom(uname);
        auto.setTo(to);
        auto.setTimestamp((int) (System.currentTimeMillis() / 1000));
        auto.setBody(body);

        ByteWriter byteWriter = new ByteWriter();
        auto.pack(byteWriter);

        writePacket(0xC00, ++nextSeq, byteWriter.toByteArray(), sessionKey, false);
    }


    private RawPacket decryptPacket(RawPacket rawPacket, byte[] key) {
        byte[] data = rawPacket.getData();
        data = crypter.decrypt(data, key);
        rawPacket.setData(data);

        System.out.println("---------------------------------------------------------");
        System.out.println("RECV: cmd=0x" + Integer.toHexString(rawPacket.getCmd()).toUpperCase());
        System.out.print("---------------------------------------------------------");
        System.out.println(HexDumper.dumpHexString(data));

        return rawPacket;
    }

    private void startKeepAlive() {
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    writePacket(0x0805, ++nextSeq, new byte[]{}, sessionKey, false);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }, 0, 20 * 1000);
    }


    private void process(RawPacket packet) throws IOException {
        //System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
        //System.out.println("cmd=" + Integer.toHexString(packet.getCmd()) + ", " + Util.convertByteToHexString(packet.getData()));
        switch (packet.getCmd()) {
            case 0x400:
                processTouchReply(packet);
                break;
            case 0x412:
                processVerifyReply(packet);
                break;
            case 0xc01:
                processNormalIM(packet);
                break;
            case 0xc00:
                processSendIMReply(packet);
                break;
        }
        //System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
    }

    private void processSendIMReply(RawPacket packet) {
        RawPacket rawPacket = decryptPacket(packet, sessionKey);
        System.out.println("send msg reply: " + rawPacket.getData()[0]);
    }


    private static byte[] passKey1(String pass) {
        return md5(pass.getBytes());
    }

    private static byte[] passKey2(String pass) {
        byte[] pp = pass.getBytes();
        byte[] dd = new byte[0x10];
        Arrays.fill(dd, (byte) 0);
        System.arraycopy(pass.getBytes(), 0, dd, 0, Math.min(pp.length, 15));
        return md5(dd);
    }

    private static byte[] nameKey3(String uname) {
        return md5(Util.unicode(uname));
    }


    public void loop() {
        RawPacket p = null;
        try {
            while ((p = in.readPacket()) != null) {
                try {
                    process(p);
                } catch (Exception e) {
                    e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    private static byte[] md5(byte[] data) {
        try {
            MessageDigest dist = MessageDigest.getInstance("md5");
            dist.update(data);
            return dist.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }


    public static void main(String[] args) throws IOException {
        RtxClient client = new RtxClient();
        client.login("192.168.27.128", 8000, "solosky", "1234");
        client.loop();

//        String pwd = "qw.123456";
//        ByteWriter byteWriter = new ByteWriter();
//        System.out.println(Util.convertByteToHexString("rongqian.xu".getBytes("unicode")));
//        System.out.println(Util.convertByteToHexString(passKey1(pwd)));
//        System.out.println(Util.convertByteToHexString(passKey2(pwd)));
//        System.out.println(Util.convertByteToHexString(Util.unicode("rongqian.xu")));
    }

//    public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
//        MessageDigest dist = MessageDigest.getInstance("md5");
//        String pass ="1234";
//        dist.update(pass.getBytes());
//        byte[] b = dist.digest();
//        System.out.println(util.Util.convertByteToHexString(b));
//
//        dist = MessageDigest.getInstance("md5");
//        byte[] dd = new byte[0x10];
//        Arrays.fill(dd, (byte)0);
//        System.arraycopy(pass.getBytes(), 0, dd, 0, pass.getBytes().length);
//
//        dist.update(dd);
//        b = dist.digest();
//        System.out.println(util.Util.convertByteToHexString(b));
//
//
//    }
}
